package com.jin.gulimall.product.service.impl;

//import io.jsonwebtoken.lang.Collections;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.jin.gulimall.product.service.CategoryBrandRelationService;
import com.jin.gulimall.product.vo.Catelog2Vo;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collector;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jin.common.utils.PageUtils;
import com.jin.common.utils.Query;

import com.jin.gulimall.product.dao.CategoryDao;
import com.jin.gulimall.product.entity.CategoryEntity;
import com.jin.gulimall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.xml.ws.soap.Addressing;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    StringRedisTemplate springRedisTemplate;

    @Autowired
    RedissonClient redisson;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        //1.查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);
        //2.组装成父子树形结构
        List<CategoryEntity> level1Menu = entities.stream().filter(categoryEntity ->
                categoryEntity.getParentCid() == 0
        ).map((menu) -> {//遍历当前菜单，并放入子分类再返回
            menu.setChildren(getChildrens(menu, entities));
            return menu;
        }).sorted((menu1, menu2) -> {//对菜单进行排序，规则就是这样
            return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
        }).collect(Collectors.toList());//将查询的结果以集合的形式保存
        return level1Menu;
    }

    @Override
    public void removeMenuByIds(List<Long> longs) {
        //TODO 检查当前要删除的菜单，是否别其他地方引用
        baseMapper.deleteBatchIds(longs);
    }

    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        List<Long> path = findParentPath(catelogId, paths);
        //利用Collections对数组元素进行逆置
        Collections.reverse(path);
        //toArray需要传递一个泛型参数
        return path.toArray(new Long[path.size()]);
    }

    @Transactional
    @Override
    public void updateDetail(CategoryEntity category) {
        this.updateById(category);
        if (!StringUtils.isEmpty(category.getName())) {
            categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
        }
    }

//    @Caching(evict = {
//            @CacheEvict(value = "category",key = "'getLevel1Category'"),
//            @CacheEvict(value = "category",key = "'getCatelogJson'")
//    })
    @CacheEvict(value = "category",allEntries = true)
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        if (!StringUtils.isEmpty(category.getName())) {
            categoryBrandRelationService.updateCategory1(category.getCatId(), category.getName());
        }
    }

    //查询出商品的所有一级分类
    //每一个需要缓存的数据我们都来指定要放到哪个名字的缓存【缓存的分区（按照业务类型分）】
    //使用注解@Cacheable代表当前方法的结果需要缓存，如果缓存中有，方法就不调用了（缓存的方法），没有则调用
    //自定义@Cacheable规则：自定义key值，自定义过期时间，自定义数据返回json格式
    @Cacheable(value = {"category"},key = "#root.methodName",sync = true)
    @Override
    public List<CategoryEntity> getLevel1Category() {
        System.out.println("getLevel1Category......");
        long l = System.currentTimeMillis();
        List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
        System.out.println("消耗的时间" + (System.currentTimeMillis() - l));
        return categoryEntities;
    }

    //TODO:产生堆外内存溢出：OutOfDirectMemoryError


    @Cacheable(value = "category",key = "#root.methodName")
    @Override
    public Map<String, List<Catelog2Vo>> getCatelogJson() throws InterruptedException {
        System.out.println("查询了数据库...");
        //查询出所有数据
        List<CategoryEntity> selectList = baseMapper.selectList(null);
        //查出一级分类
        List<CategoryEntity> level1Category = getParent_cid(selectList, 0L);
        //查出该一级分类对应的二级分类
        Map<String, List<Catelog2Vo>> map = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
            List<Catelog2Vo> catelog2Vos = null;
            if (categoryEntities != null) {
                //将二级分类封装成对应的类型
                catelog2Vos = categoryEntities.stream().map(lv2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, lv2.getCatId().toString(), lv2.getName());
                    //查出二级分类对应的三级分类并进行封装
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList, lv2.getCatId());
                    if (level3Catelog != null) {
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(lv3 -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(lv2.getCatId().toString(), lv3.getCatId().toString(), lv3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
        return map;
    }

    /**
     * 1).springboot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信
     * 2).lettuce的bug导致netty堆外内存溢出 -Xmx300m netty如果没有指定堆外内存，默认使用 -Xmx300m
     * 可以通过-Dio.netty.maxDirectMemory进行设置
     * 方案：不能使用 -Dio.netty.maxDirectMemory只去调大堆外内存
     * 1>升级lettuce客户端 2>切换使用jedis
     * redisTemplate:
     * lettuce,jedis操作redis的底层客户端。Spring对lettuc,jedis再次封装得到redisTemplate;
     */

    //@Override
    public Map<String, List<Catelog2Vo>> getCatelogJson2() {
        //给缓存中放json字符串，拿出的json字符串，再逆转为能用的对象类型（序列化与反序列化）
        //缓存中存储的都是json数据，json跨平台语言
        String catalogJson = springRedisTemplate.opsForValue().get("catalogJson");
        if (StringUtils.isEmpty(catalogJson)) {
            System.out.println("缓存不命中...将要查询数据库...");
            //若缓存中为空，则从数据库中查询数据
            Map<String, List<Catelog2Vo>> catelogJsonFromDb = getCatelogJsonFromDbWithRedisLock();
            //查到的数据再放入缓存，将对象转为json放在缓存中
            String s = JSON.toJSONString(catelogJsonFromDb);
            springRedisTemplate.opsForValue().set("catalogJson", s);
            return catelogJsonFromDb;
        }
        System.out.println("缓存命中...直接返回数据...");
        Map<String, List<Catelog2Vo>> map = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
        });
        return map;
    }

    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedissonLock() {
        RLock lock = redisson.getLock("catelogJson");
        lock.lock();
        Map<String, List<Catelog2Vo>> dataFromDb;
        try {
            dataFromDb = getDataFromDb();
        } finally {
            lock.unlock();
        }
        return dataFromDb;
    }

    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedisLock(){
        while(true){
            String uuid = UUID.randomUUID().toString();
            Boolean lock = springRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
            if(lock){
                //加锁成功
//            String lockValue = springRedisTemplate.opsForValue().get("lock");
//            if(lockValue.equals(uuid)){
//                springRedisTemplate.delete("lock");
//            }
                Map<String, List<Catelog2Vo>> dataFromDb;
                //使用try-finally:这样就不用自动续期，设置较长时间的过期时间，然后都要执行删除锁
                try{
                    dataFromDb = getDataFromDb();
                }finally {
                    String script="if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                            "then\n" +
                            "    return redis.call(\"del\",KEYS[1])\n" +
                            "else\n" +
                            "    return 0\n" +
                            "end";
                    //Lua脚本，要么不做要么全做
                    springRedisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class), Arrays.asList("lock"), uuid);
                }
                return dataFromDb;
            }
        }
    }
    //采用自旋的方式（递归调用）
    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithRedisLock1(){
        String uuid = UUID.randomUUID().toString();
        Boolean lock = springRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if(lock){
            //加锁成功
//            String lockValue = springRedisTemplate.opsForValue().get("lock");
//            if(lockValue.equals(uuid)){
//                springRedisTemplate.delete("lock");
//            }
            Map<String, List<Catelog2Vo>> dataFromDb;
            //使用try-finally:这样就不用自动续期，设置较长时间的过期时间，然后都要执行删除锁
            try{
                dataFromDb = getDataFromDb();
            }finally {
                String script="if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                //Lua脚本，要么不做要么全做
                springRedisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class), Arrays.asList("lock"), uuid);
            }
            return dataFromDb;
        }else{
            //加锁成功---重试
            //设置休眠时间
            try{
                Thread.sleep(1000);
            }catch (Exception e){

            }
            return getCatelogJsonFromDbWithRedisLock();//自旋
        }
    }

    //采用while循环调用
    private Map<String, List<Catelog2Vo>> getDataFromDb() {
        //获得锁进入后要再次查询缓存看是否有数据
        String catalogJson = springRedisTemplate.opsForValue().get("catalogJson");
        if (!StringUtils.isEmpty(catalogJson)) {
            Map<String, List<Catelog2Vo>> map = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
            return map;
        }
        System.out.println("查询了数据库...");
        //查询出所有数据
        List<CategoryEntity> selectList = baseMapper.selectList(null);
        //查出一级分类
        List<CategoryEntity> level1Category = getParent_cid(selectList, 0L);
        //查出该一级分类对应的二级分类
        Map<String, List<Catelog2Vo>> map = level1Category.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
            List<CategoryEntity> categoryEntities = getParent_cid(selectList, v.getCatId());
            List<Catelog2Vo> catelog2Vos = null;
            if (categoryEntities != null) {
                //将二级分类封装成对应的类型
                catelog2Vos = categoryEntities.stream().map(lv2 -> {
                    Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, lv2.getCatId().toString(), lv2.getName());
                    //查出二级分类对应的三级分类并进行封装
                    List<CategoryEntity> level3Catelog = getParent_cid(selectList, lv2.getCatId());
                    if (level3Catelog != null) {
                        List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(lv3 -> {
                            Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(lv2.getCatId().toString(), lv3.getCatId().toString(), lv3.getName());
                            return catelog3Vo;
                        }).collect(Collectors.toList());
                        catelog2Vo.setCatalog3List(collect);
                    }
                    return catelog2Vo;
                }).collect(Collectors.toList());
            }
            return catelog2Vos;
        }));
        //放入缓存中，加锁，这些是原子操作
        String s = JSON.toJSONString(map);
        springRedisTemplate.opsForValue().set("catalogJson", s);
        return map;
    }

    public Map<String, List<Catelog2Vo>> getCatelogJsonFromDbWithLocalLock() {
        synchronized (this){
            //获得锁进入后要再次查询缓存看是否有数据
            return getDataFromDb();
        }
    }

    private List<CategoryEntity> getParent_cid(List<CategoryEntity> selectList,Long parent_cid) {
        List<CategoryEntity> collect = selectList.stream().filter(item -> item.getParentCid().equals(parent_cid)).collect(Collectors.toList());
        //return baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", v.getCatId()));
        return collect;
    }

    private List<Long> findParentPath(Long catelogId,List<Long> paths){
        paths.add(catelogId);
        CategoryEntity entity = this.getById(catelogId);
        if(entity!=null){
            if(entity.getParentCid()!=0){
                findParentPath(entity.getParentCid(), paths);
            }
        }

        return paths;
    }

    //递归查找所有菜单的子菜单
    //因为是三级菜单，所以查找一次，递归两次，一共三次
    private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all){
        List<CategoryEntity> collect = all.stream().filter(categoryEntity ->
            categoryEntity.getParentCid().equals(root.getCatId())
        ).map((menu)->{
            menu.setChildren(getChildrens(menu,all));
            return menu;
        }).sorted((menu1,menu2)->{
            return (menu1.getSort()==null?0:menu1.getSort())-(menu2.getSort()==null?0:menu2.getSort());
        }).collect(Collectors.toList());
        return collect;
    }

}